﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using Red.Controls.Curve.ViewModels;
using Red.Controls.TimeLine.Core;

namespace Red.Controls.Curve
{
    public class TrackControl : Adorner
    {
        #region Data

        private int _selectedPointIndex = -1;
        private int _pointsPerSegment = 2;

        // this array here is an equivalent of the DataItem.Children of track data item
        private ObservableCollection<ControlPoint> _points = null;

        private ObservableCollection<CircleControl> _circles;
        private ObservableCollection<CurveSegment> _segments;

        private readonly VisualCollection _visualCollection;

        #endregion

        #region Properties

        public CurveControl ParentCurve { get; private set; }

        public Brush CurveColor { get; set; }

        public TimeRuler HorizontalRuler { get; private set; }
        public TimeRuler VerticalRuler { get; private set; }

        public static readonly DependencyProperty LockedProperty = DependencyProperty.Register( "Locked",
            typeof( bool ), typeof( TrackControl ), new FrameworkPropertyMetadata( false, LockedProperty_PropertyChanged ) );
        public bool Locked
        {
            get { return (bool)GetValue( LockedProperty ); }
            set { SetValue( LockedProperty, value ); }
        }

        #endregion

        #region C-tor

        public TrackControl( CurveControl parent, Brush color, TimeRuler timeRuler, TimeRuler valueRuler )
            : base( parent.AdornedElement )
        {
            ParentCurve = parent;
            CurveColor = color;
            HorizontalRuler = timeRuler;
            VerticalRuler = valueRuler;

            _visualCollection = new VisualCollection( this );

            _circles = new ObservableCollection<CircleControl>();
            _circles.CollectionChanged += Circles_CollectionChanged;

            _segments = new ObservableCollection<CurveSegment>();
            _segments.CollectionChanged += Segments_CollectionChanged;

            DataContextChanged += TrackControl_DataContextChanged;
        }

        private void TrackControl_DataContextChanged( object sender, DependencyPropertyChangedEventArgs e )
        {
            if ( _points != null )
            {
                _points.CollectionChanged -= Points_CollectionChanged;
            }

            var dc = e.NewValue as TrackViewModel;
            if ( dc != null )
            {
                _points = dc.Points;
                OnPointsAdded( dc.Points );

                _points.CollectionChanged += Points_CollectionChanged;
            }
            else
            {
                _points = null;
            }
        }

        static void LockedProperty_PropertyChanged( DependencyObject obj, DependencyPropertyChangedEventArgs e )
        {
            TrackControl tc = obj as TrackControl;
            if ( tc != null )
            {
                tc.Lock( (bool)e.NewValue );
            }
        }

        private void Lock( bool locked = true )
        {
            foreach ( var point in _circles )
            {
                point.Locked = locked;
            }
        }


        #endregion

        #region Public methods
        public void Rearrange()
        {
            ArrangeOverride( AdornedElement.RenderSize );
        }

        public void AddPoint( Point p )
        {
            if ( Locked )
            {
                return;
            }

            double time = HorizontalRuler.StartValue + p.X / HorizontalRuler.PixelToValueRatio;
            double value = VerticalRuler.StartValue + ( AdornedElement.RenderSize.Height - p.Y ) / VerticalRuler.PixelToValueRatio;

            HandleDataChange();

            _points.Add( new ControlPoint( time, value, ParentCurve.InterpolationType ) );
        }

        public void RemovePoint( double time )
        {
            if ( _circles.Count == 1 || Locked )
            {
                return;
            }

            var toRemove = _points.FirstOrDefault( el => el.Time == time );
            if ( toRemove != null )
            {
                _points.Remove( toRemove );
            }

            HandleDataChange();
        }

        public void ChangeInterpolationType( EInteroplationType newType )
        {
            foreach ( var point in _points )
            {
                point.Type = newType;
            }
            
            if ( newType == EInteroplationType.CPT_BezierQuadratic )
            {
                _pointsPerSegment = 3;
            }
            else if ( newType == EInteroplationType.CPT_BezierCubic )
            {
                _pointsPerSegment = 4;
            }
            else
            {
                _pointsPerSegment = 2;
            }

            ClearSegments();

            int step = _pointsPerSegment - 1;
            for ( int i = 0; i < _points.Count; i += step )
            {
                AddSegment( _points[i] );
            }
        }

        #endregion

        #region Framework element overrides

        protected override int VisualChildrenCount
        {
            get { return _visualCollection.Count; }
        }

        protected override Visual GetVisualChild( int index )
        {
            return _visualCollection[index];
        }

        protected override Size ArrangeOverride( Size finalSize )
        {
            double controlWidth = AdornedElement.RenderSize.Width;
            double controlHeight = AdornedElement.RenderSize.Height;


            foreach ( var seg in _segments )
            {
                seg.Arrange( controlWidth, controlHeight );
            }

            foreach ( var circle in _circles )
            {
                circle.Arrange( controlWidth, controlHeight );
            }

            return finalSize;
        }

        #endregion

        #region Event handlers

        private void Circles_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            if ( e.Action == NotifyCollectionChangedAction.Move )
            {
                return;
            }

            if ( e.NewItems != null )
            {
                foreach ( var item in e.NewItems )
                {
                    CircleControl cs = item as CircleControl;
                    _visualCollection.Add( cs );

                    cs.MouseLeftButtonDown += EllipseShape_MouseLeftButtonDown;
                    cs.MouseDown += EllipseShape_MouseDown;
                    cs.OnMoved = ( cc ) =>
                    {
                        ParentCurve.OnPointMoved( _points.IndexOf( cc as ControlPoint ), cc.Time );
                    };

                    cs.OnSelection = OnCurvePointSelected;
                    cs.OnMovementFinsihed = OnCurvePointMovementFinished;

                    cs.Arrange( AdornedElement.RenderSize.Width, AdornedElement.RenderSize.Height );
                }
            }

            if ( e.OldItems != null )
            {
                foreach ( var item in e.OldItems )
                {
                    CircleControl cs = item as CircleControl;
                    _visualCollection.Remove( cs );
                }
            }
        }

        private void ControlPointsVisuals_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            if ( e.Action == NotifyCollectionChangedAction.Move )
            {
                return;
            }

            if ( e.NewItems != null )
            {
                foreach ( var item in e.NewItems )
                {
                    CircleControl cs = item as CircleControl;
                    _visualCollection.Add( cs );

                    cs.MouseDown += EllipseShape_MouseDown;
                    cs.Arrange( AdornedElement.RenderSize.Width, AdornedElement.RenderSize.Height );
                }
            }

            if ( e.OldItems != null )
            {
                foreach ( var item in e.OldItems )
                {
                    CircleControl cs = item as CircleControl;
                    _visualCollection.Remove( cs );
                }

                ArrangeOverride( AdornedElement.RenderSize );
            }
        }

        private void Segments_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            if ( e.Action == NotifyCollectionChangedAction.Move )
            {
                return;
            }
            else if ( e.Action == NotifyCollectionChangedAction.Reset )
            {
                List<Visual> toRemove = new List<Visual>();
                foreach ( var vis in _visualCollection )
                {
                    if ( vis as CircleControl == null )
                    {
                        toRemove.Add( vis );
                    }
                }

                foreach ( var vis in toRemove )
                {
                    _visualCollection.Remove( vis );
                }
                return;
            }

            if ( e.NewItems != null )
            {
                foreach ( var item in e.NewItems )
                {
                    CurveSegment seg = item as CurveSegment;

                    foreach ( var vis in seg.VisualElement )
                    {
                        vis.MouseLeftButtonDown += CurveSegment_MouseLeftButtonDown;
                        vis.ClipToBounds = true;
                        _visualCollection.Insert( 0, vis );
                    }
                    seg.Arrange( AdornedElement.RenderSize.Width, AdornedElement.RenderSize.Height );
                }
            }

            if ( e.OldItems != null )
            {
                foreach ( var item in e.OldItems )
                {
                    CurveSegment seg = item as CurveSegment;
                    seg.Dispose();
                    foreach ( var vis in seg.VisualElement )
                    {
                        vis.MouseLeftButtonDown -= CurveSegment_MouseLeftButtonDown;
                        _visualCollection.Remove( vis );
                    }
                }

                ArrangeOverride( AdornedElement.RenderSize );
            }
        }

        private void Points_CollectionChanged( object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e )
        {
            if ( e.Action == NotifyCollectionChangedAction.Move )
            {
                OnPointsOrderChanged( e.NewStartingIndex, e.OldStartingIndex );
                return;
            }
            else if ( e.Action == NotifyCollectionChangedAction.Reset )
            {
                ClearSegments();
                _circles.Clear();
                _visualCollection.Clear();
            }

            if ( e.NewItems != null )
            {
                OnPointsAdded( e.NewItems );
            }

            if ( e.OldItems != null )
            {
                OnPointsRemoved( e.OldItems, e.OldStartingIndex );
            }
        }

        private void EllipseShape_MouseLeftButtonDown( object sender, MouseButtonEventArgs e )
        {
            if ( e.ClickCount == 2 )
            {
                ParentCurve.RemovePoint( ( sender as CircleControl ).SourcePoint.Time );
            }
        }

        private void EllipseShape_MouseDown( object sender, MouseButtonEventArgs e )
        {
            ParentCurve.Activate( this );
        }

        private void CurveSegment_MouseLeftButtonDown( object sender, MouseButtonEventArgs e )
        {
            ParentCurve.Activate( this );
        }

        #endregion

        #region Private methods

        private bool SortPoints()
        {
            bool orderChanged = false;

            var orderedPoints = _points.OrderBy( pm => pm.Time ).ToList();
            for ( int i = 0; i < orderedPoints.Count; ++i )
            {
                if ( orderedPoints[i] != _points[i] )
                {
                    _points.Move( _points.IndexOf( orderedPoints[i] ), i );
                    orderChanged = true;
                }
            }

            return orderChanged;
        }

        private int GetSegmentIndexByPoint( int pointInd )
        {
            int pointsStartStep = _pointsPerSegment - 1;
            int segmentInd = pointInd / pointsStartStep;

            if ( pointInd % pointsStartStep != 0 )
            {
                return segmentInd;
            }

            // it's a first point of a segment - it influences also previous segment
            // (unless it's a first segment)
            return Math.Max( 0, segmentInd - 1 );
        }

        private int GetSegmentStartingPointInd( int segmentInd )
        {
            return segmentInd * ( _pointsPerSegment - 1 );
        }

        private void AddNewCircle( ControlPoint sourcePoint )
        {
            _circles.Add( new CircleControl( sourcePoint, AdornedElement, HorizontalRuler, VerticalRuler ) );
        }

        private void RemoveCircle( ControlPoint sourcePoint )
        {
            // remove point's visualization
            CircleControl shape = _circles.FirstOrDefault( cs => cs.SourcePoint == sourcePoint );
            if ( shape != null )
            {
                _circles.Remove( shape );
            }
        }

        private void UpdateSegmentEnd( ControlPoint start, ControlPoint newEndPoint )
        {
            CurveSegment segment = _segments.FirstOrDefault( seg => seg.Start == start );
            if ( segment != null )
            {
                segment.End = newEndPoint;
            }
            else
            {
                AddSegment( start );
            }
        }

        private void OnLinearCurvePointAppended( IList newItems )
        {
            for ( int i = _points.Count - newItems.Count; i < _points.Count; ++i )
            {
                if ( i > 0 )
                {
                    // just update previous point's segment
                    UpdateSegmentEnd( _points[i - 1], _points[i] );
                }
            }
        }

        private void OnSegmentedCurvePointAppended( IList newItems )
        {
            int startingIndex = _points.Count - newItems.Count;
            int firstNewSegment = GetSegmentIndexByPoint( startingIndex );
            startingIndex = GetSegmentStartingPointInd( firstNewSegment );

            int step = _pointsPerSegment - 1;

            // we have enough points to add new segment
            for ( int i = startingIndex; i < _points.Count; i += step )
            {
                AddSegment( _points[i] );
            }
        }

        private void OnPointsAdded( IList newItems )
        {
            Dispatcher.BeginInvoke( (Action)delegate ()
            {
                bool orderChanged = SortPoints();

                foreach ( var item in newItems )
                {
                    ControlPoint pm = item as ControlPoint;

                    if ( !orderChanged )
                    {
                        if ( _pointsPerSegment == 2 )
                        {
                            OnLinearCurvePointAppended( newItems );
                        }
                        else
                        {
                            OnSegmentedCurvePointAppended( newItems );
                        }
                    }

                    // add point's representation
                    AddNewCircle( pm );
                }
            } );
        }


        private void OnLinearCurvePointsOrderChanged( int pointInd, int pointIndBefore )
        {
            int prevInd = pointIndBefore - 1;
            if ( prevInd >= pointInd )
            {
                prevInd++;
            }
            int nextInd = pointIndBefore + 1;
            if ( nextInd <= pointInd )
            {
                nextInd--;
            }

            // find neighbour points before and after change
            var prevPoint = prevInd < 0 ? null : _points[prevInd];
            var nextPoint = nextInd >= _points.Count ? null : _points[nextInd];
            ControlPoint destPrevPoint = prevPoint, destNextPoint = nextPoint;

            ControlPoint sourcePoint = _points[pointInd] as ControlPoint;

            if ( ( prevPoint != null && sourcePoint.Time < prevPoint.Time ) || ( nextPoint != null && sourcePoint.Time > nextPoint.Time ) )
            {
                destPrevPoint = pointInd == 0 ? null : _points[pointInd - 1];
                destNextPoint = pointInd == _points.Count - 1 ? null : _points[pointInd + 1];
            }

            // first, if moving this point changed points order, connect the points that were previously its neighbours
            if ( prevPoint != null && nextPoint != null )
            {
                UpdateSegmentEnd( prevPoint, nextPoint );
            }

            // then update the segment that starts in the moved point
            if ( destNextPoint != null )
            {
                UpdateSegmentEnd( sourcePoint, destNextPoint );
            }
            else
            {
                // ...or just remove this segment if source point became the last point
                _segments.Remove( _segments.First( el => el.Start == sourcePoint ) );
            }

            // in the end update the segment that is supposed to end in the moved point...
            if ( destPrevPoint != null )
            {
                UpdateSegmentEnd( destPrevPoint, sourcePoint );
            }

            // ... and remove the segments that had its end in the source if it was the last segment
            if ( nextPoint == null )
            {
                _segments.Remove( _segments.FirstOrDefault( el => el.Start == prevPoint ) );
            }
        }

        private void OnSegmentedCurvePointsOrderChange( int pointInd, int pointIndBefore )
        {
            int step = _pointsPerSegment - 1;

            //remove influenced segments
            int influencedSegmentInd = GetSegmentIndexByPoint( Math.Min( pointInd, pointIndBefore ) );
            int startingPoint = GetSegmentStartingPointInd( influencedSegmentInd );

            int removeInd = startingPoint;
            while ( removeInd < _points.Count )
            {
                RemoveSegment( removeInd );
                removeInd++;
            }

            // ...and then add changed segments
            for ( int segStartInd = startingPoint; segStartInd < _points.Count; segStartInd += step )
            {
                AddSegment( _points[segStartInd] );
            }
        }

        private void OnPointsOrderChanged( int pointInd, int pointIndBefore )
        {
            if ( pointInd != pointIndBefore )
            {
                if ( _pointsPerSegment == 2 )
                {
                    OnLinearCurvePointsOrderChanged( pointInd, pointIndBefore );
                }
                else
                {
                    OnSegmentedCurvePointsOrderChange( pointInd, pointIndBefore );
                }
            }
        }

        private void RemoveSegment( int startPointIndex )
        {
            if ( startPointIndex >= 0 && startPointIndex < _points.Count )
            {
                var seg = _segments.FirstOrDefault( el => el.Start == _points[startPointIndex] );
                _segments.Remove( seg );
            }
        }
        private void ClearSegments()
        {
            foreach ( var seg in _segments )
            {
                seg.Dispose();
            }

            _segments.Clear();
        }

        private void OnPointsRemoved( IList oldItems, int startingIndex )
        {
            foreach ( var item in oldItems )
            {
                ControlPoint pm = item as ControlPoint;

                if ( _pointsPerSegment == 2 )
                {
                    // remove point's child segments
                    var postSegment = _segments.ToList().FirstOrDefault( seg => seg.Start == pm );

                    ControlPoint nextPoint = postSegment?.End;
                    ControlPoint prevPoint = null;

                    _segments.Remove( postSegment );
                    
                    if ( nextPoint != null )
                    {
                        int nextPointInd = _points.IndexOf( nextPoint );
                        prevPoint = nextPointInd > 0 ? _points[nextPointInd - 1] : null;

                        // update segments for previous point
                        if ( prevPoint != null )
                        {
                            var prevSeg = _segments.First( seg => seg.Start == prevPoint );
                            prevSeg.End = nextPoint;
                        }
                    }
                    else
                    {
                        var prevSegment = _segments.ToList().FirstOrDefault( seg => seg.End == pm );
                        _segments.Remove( prevSegment );
                    }
                }
                else
                {
                    int prevPoint = Math.Max( 0, startingIndex - 1 );
                    OnPointsOrderChanged( prevPoint, startingIndex );
                }

                RemoveCircle( pm );
            }

            ArrangeOverride( AdornedElement.RenderSize );
        }

        private void OnCurvePointSelected( CircleControl controlPoint )
        {
            ControlPoint sourcePoint = controlPoint.SourcePoint as ControlPoint;
            _selectedPointIndex = _points.IndexOf( sourcePoint );
        }

        private void OnCurvePointMovementFinished( ControlPoint controlPoint )
        {
            if ( _selectedPointIndex >= 0 )
            {
                HandleDataChange();
            }
        }

        public void OnCurvePointMovementFinished( int pointIndBefore, double newTime )
        {
            ControlPoint sourcePoint = _points[pointIndBefore] as ControlPoint;
            sourcePoint.Time = newTime;
            SortPoints();
        }

        private void HandleDataChange()
        {
            // send data blob to interop?
        }
        
        private CurveSegment CreateSegment( ControlPoint startPoint, Brush color )
        {
            int pointIndex = _points.IndexOf( startPoint );

            // do not create segment for last point
            if ( pointIndex == _points.Count - 1 )
            {
                return null;
            }

            var nextPoint = _points[pointIndex + 1];
            if ( startPoint.Type == EInteroplationType.CPT_Constant )
            {
                return new ConstCurveSegment( startPoint, nextPoint, color, HorizontalRuler, VerticalRuler, AdornedElement );
            }
            else if ( startPoint.Type == EInteroplationType.CPT_Linear )
            {
                return new LinearCurveSegment( startPoint, nextPoint, color, HorizontalRuler, VerticalRuler, AdornedElement );
            }
            else if ( startPoint.Type == EInteroplationType.CPT_BezierQuadratic )
            {
                if ( pointIndex + 2 < _points.Count )
                {
                    return new BezierQuadraticSegment( startPoint, _points[pointIndex + 2], nextPoint, color, HorizontalRuler, VerticalRuler, AdornedElement );
                }
            }
            else if ( startPoint.Type == EInteroplationType.CPT_BezierCubic )
            {
                if ( pointIndex + 3 < _points.Count )
                {
                    return new BezierCubicSegment( startPoint, _points[pointIndex + 3], nextPoint, _points[pointIndex + 2], color, HorizontalRuler, VerticalRuler, AdornedElement );
                }
            }
            return null;
        }

        private bool AddSegment( ControlPoint start )
        {
            var seg = CreateSegment( start, CurveColor );

            if ( seg != null )
            {
                _segments.Add( seg );
                return true;
            }

            return false;
        }

        #endregion

    }
}
